package org.test4j.mock.faking.meta;

import g_asm.org.objectweb.asm.Type;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
import org.test4j.mock.Mock;
import org.test4j.mock.faking.util.ClassLoad;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;

import static g_asm.org.objectweb.asm.Type.getMethodDescriptor;
import static org.test4j.mock.faking.util.ReflectUtility.getCorrespondingFakeName;
import static org.test4j.mock.faking.util.TypeDesc.T_Invocation;
import static org.test4j.mock.faking.util.TypeUtility.classPath;

/**
 * 从class文件中解析出的方法元数据
 *
 * @author darui.wu
 */
@Accessors(chain = true)
@EqualsAndHashCode(of = {"methodDesc"})
public class MethodId {
    /**
     * 声明mock行为的类 new MockUp<realClass>
     */
    public final String realClassDesc;

    public int targetHashCode = 0;
    /**
     * 实际实现方法逻辑的类, 可以是realClass或其父类
     * null: 表示是抽象方法, 在realClass和其父类中无实现逻辑
     */
    public final String declaredClassDesc;

    public final String name;

    public final String desc;
    /**
     * Mock方法参数是否包含{@link org.test4j.mock.Invocation}
     */
    public final boolean hasInvocation;
    /**
     * @Mock 方法签名(如果 desc 第一个参数是Invocation, 去掉 Invocation参数)
     */
    public final String descNoInvocation;

    /**
     * 是否定义了 {@link Mock} 注解的方法
     */
    @Getter
    @Setter
    private boolean isMockMethod = false;
    /**
     * method name + para desc
     */
    public final String methodDesc;

    public MethodId(Class realClassDesc, Method method) {
        this(realClassDesc, method.getDeclaringClass(), method.getName(), getMethodDescriptor(method));
    }

    public MethodId(Class realClassDesc, Class declaredClassDesc, String name, String desc) {
        this(classPath(realClassDesc), classPath(declaredClassDesc), name, desc);
    }

    public MethodId(String realClassDesc, String declaredClassDesc, String name, String desc) {
        this.realClassDesc = realClassDesc;
        this.declaredClassDesc = declaredClassDesc;
        this.name = name;
        this.desc = desc;
        this.hasInvocation = this.desc.contains(T_Invocation.DESC);
        if (this.hasInvocation) {
            descNoInvocation = '(' + this.desc.substring(T_Invocation.DESC.length() + 1);
        } else {
            descNoInvocation = this.desc;
        }
        this.methodDesc = buildMethodDesc(this.name, this.descNoInvocation);
    }

    public static String buildMethodDesc(String name, String desc) {
        String _name = getCorrespondingFakeName(name);
        int end = desc.lastIndexOf(')') + 1;
        if (desc.contains(T_Invocation.DESC)) {
            return _name + '(' + desc.substring(T_Invocation.DESC.length() + 1, end);
        } else {
            return _name + desc.substring(0, end);
        }
    }

    private Type[] params = null;

    public static String getName(String methodDesc) {
        int index = methodDesc.indexOf("(");
        return methodDesc.substring(0, index);
    }

    public static String getDesc(String methodDesc) {
        int index = methodDesc.indexOf("(");
        return methodDesc.substring(index);
    }

    public Type[] getParams() {
        try {
            if (params == null) {
                params = Type.getArgumentTypes(desc);
            }
            return this.params;
        } catch (Throwable e) {
            throw new RuntimeException("getParams[" + desc + "] error:" + e.getMessage(), e);
        }
    }

    public String[] getParaTypeNames() {
        return Stream.of(this.getParams())
            .map(Type::getClassName)
            .toArray(String[]::new);
    }

    private Type returnType = null;

    public Type getReturnType() {
        if (returnType == null) {
            this.returnType = Type.getReturnType(desc);
        }
        return this.returnType;
    }

    private Class realClass;

    public Class realClass() {
        if (realClass == null) {
            this.realClass = ClassLoad.loadClass(this.realClassDesc);
        }
        return realClass;
    }

    public MethodId setTarget(Class declaredClass, Object target) {
        this.realClass = declaredClass;
        this.targetHashCode = target == null ? 0 : System.identityHashCode(target);
        return this;
    }

    /**
     * 参数列表
     */
    private Map<Integer, ParaNameType> parameters;

    public Map<Integer, ParaNameType> getParameters() {
        if (this.parameters != null) {
            return this.parameters;
        }
        this.parameters = new HashMap<>();
        Type[] types = Type.getArgumentTypes(this.descNoInvocation);
        for (int index = 0; index < types.length; index++) {
            this.parameters.put(index, new ParaNameType(String.format("arg%d", index), types[index].getDescriptor()));
        }
        return this.parameters;
    }

    public void addParaNameType(int index, String name, String type) {
        if (this.parameters == null) {
            this.parameters = new HashMap<>();
        }
        parameters.put(index, new ParaNameType(name, type));
    }

    @ToString
    public static class ParaNameType {
        public final String name;

        public final String type;

        public ParaNameType(String name, String type) {
            this.name = name;
            this.type = type;
        }
    }
}